/*
 * Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "qmidev.h"

#include <assert.h>
#include <errno.h>
#include <glib.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <glib.h>

#include <libqmi.h>

#include "dev.h"
#include "error.h"
#include "file.h"
#include "mock.h"
#include "poller.h"
#include "qmi.h"
#include "qmictl.h"
#include "qmidev.h"
#include "qmidms.h"
#include "qmimsg.h"
#include "qmiwds.h"
#include "util.h"

static const int qmidev_log_unhandled_messages = 0;

struct qmidev {
  void *priv;

  struct poller *poller;

  struct dev *dev;
  struct polled *dev_polled;

  GList *callbacks;
  GList *listeners;
  GList *clients;

  int connected;
};

struct callback {
  callback_fn func;
  void *context;
};

struct listener {
  struct client *client;
  struct listen_criteria criteria;
  qmidev_listen_callback_fn callback;
  void *context;
};

struct client {
  struct service *service;

  uint8_t client_id;
  uint16_t next_transaction_id;
  struct listener *event_listener;
  GList *events;

  void *priv;
};

static int service_is_control(struct service *service)
{
  return !service->service_id;
}

static int call_listeners(struct qmidev *qmidev, struct qmimsg *message);
static void call_callbacks(struct qmidev *qmidev);

static struct qmidev *qmidev_new(void)
{
  struct qmidev *qmidev = g_slice_new(struct qmidev);

  qmidev->priv = NULL;

  qmidev->poller = poller_new();
  qmidev->dev = NULL;
  qmidev->dev_polled = NULL;

  qmidev->callbacks = NULL;
  qmidev->listeners = NULL;
  qmidev->clients = NULL;

  qmidev->connected = 0;

  return qmidev;
}

struct qmidev *qmidev_new_file(const char *path)
{
  struct qmidev *qmidev;
  struct dev *dev;

  assert(path);

  qmidev = qmidev_new();
  dev = file_new(qmidev, path);
  if (!dev) {
    free(qmidev);
    return NULL;
  }
  qmidev->dev = dev;

  return qmidev;
}

struct qmidev *qmidev_new_mock(void)
{
  struct qmidev *qmidev;
  struct dev *dev;

  qmidev = qmidev_new();
  dev = mock_new();
  qmidev->dev = dev;

  return qmidev;
}

void *qmidev_priv(struct qmidev *qmidev)
{
  assert(qmidev);
  return qmidev->priv;
}

void qmidev_set_priv(struct qmidev *qmidev, void *priv)
{
  assert(qmidev);
  qmidev->priv = priv;
}

int qmidev_fd(struct qmidev *qmidev)
{
  assert(qmidev);
  return poller_fd(qmidev->poller);
}

int qmidev_process(struct qmidev *qmidev)
{
  assert(qmidev);

  call_callbacks(qmidev);
  return poller_poll(qmidev->poller);
}

struct listener *qmidev_listen(struct qmidev *qmidev,
                               struct client *client,
                               struct listen_criteria *criteria,
                               qmidev_listen_callback_fn callback,
                               void *context)
{
  struct listener *listener;

  assert(qmidev);
  assert(criteria);
  assert(callback);

  listener = g_slice_new(struct listener);
  listener->client = client;
  listener->criteria = *criteria;
  listener->callback = callback;
  listener->context = context;

  qmidev->listeners = g_list_prepend(qmidev->listeners, listener);

  return listener;
}

void qmidev_cancel_listen(struct qmidev *qmidev, struct listener *listener)
{
  assert(qmidev);
  assert(listener);

  qmidev->listeners = g_list_remove(qmidev->listeners, listener);
  g_slice_free(struct listener, listener);
}

struct call_listener_context {
  struct qmidev *qmidev;
  struct qmimsg *message;
  uint8_t msg_service;
  uint8_t msg_client;
  uint8_t msg_qmi_flags;
  uint16_t msg_transaction;
  uint16_t msg_message;
  int calls;
};

static void call_listener(void *data, void *user_data)
{
  struct call_listener_context *context = user_data;
  struct listener *listener = data;
  struct listen_criteria *crit = &listener->criteria;
  int flags = crit->flags;
  int result;

  if (listener->client->service->service_id != context->msg_service)
    return;

  if ((context->msg_client != QMI_CID_BROADCAST) &&
      (listener->client->client_id != context->msg_client))
    return;

  if ((flags & LISTEN_MATCH_QMI_FLAGS) &&
      (crit->qmi_flags != context->msg_qmi_flags))
    return;

  if ((flags & LISTEN_MATCH_TRANSACTION) &&
      (crit->transaction != context->msg_transaction))
    return;

  if ((flags & LISTEN_MATCH_MESSAGE) &&
      (crit->message != context->msg_message))
    return;

  result = listener->callback(context->qmidev,
                              listener->context,
                              context->message);
  if (result) {
    /* The glib documentation does not note this, but it is in fact safe to
       call g_list_remove while called back from g_list_foreach. */
    qmidev_cancel_listen(context->qmidev, listener);
  }

  context->calls++;
}

static int call_listeners(struct qmidev *qmidev, struct qmimsg *message)
{
  struct call_listener_context context;
  context.qmidev = qmidev;
  context.message = message;
  qmimsg_get_header(message,
                    &context.msg_service,
                    &context.msg_client,
                    &context.msg_qmi_flags,
                    &context.msg_transaction,
                    &context.msg_message);
  context.calls = 0;
  g_list_foreach(qmidev->listeners, &call_listener, &context);
  return context.calls == 0;
}



static struct client *make_ctl_client(struct qmidev *qmidev)
{
  struct client *client = g_slice_new(struct client);

  client->service = &qmictl_service;
  client->client_id = 0x00;
  client->next_transaction_id = 0x0001;
  client->events = NULL;
  client->priv = NULL;

  if (client->service->create_fn)
    client->service->create_fn(qmidev, client);

  return client;
}

struct transaction {
  struct client *client;
  uint16_t transaction_id;

  struct listener *response_listener;
  response_fn callback;
  void *context;
};

struct call_event_listeners_context {
  struct qmidev *qmidev;
  struct client *client;
  uint8_t type;
  uint16_t length;
  void *value;
  int calls;
};

static void call_event_listener(void *data, void *user_data)
{
  struct call_event_listeners_context *context = user_data;
  struct event *event = data;

  if (event->type != context->type)
    return;

  event->callback(context->qmidev,
                  event->user_callback,
                  event->user_context,
                  context->length,
                  context->value);

  context->calls++;
}

static void call_event_listeners(void *data,
                                 uint8_t type, uint16_t length, void *value)
{
  struct call_event_listeners_context *context = data;

  context->type = type;
  context->length = length;
  context->value = value;
  context->calls = 0;

  g_list_foreach(context->client->events, &call_event_listener, context);
}

static int event_message_received(struct qmidev *qmidev,
                                   void *data,
                                   struct qmimsg *message)
{
  struct call_event_listeners_context context;

  context.qmidev = qmidev;
  context.client = data;
  qmimsg_tlv_foreach(message, &call_event_listeners, &context);

  return 0;
}

struct get_client_context {
  struct client *client;
  qmidev_client_fn callback;
  void *context;
};

static void got_cid(struct qmidev *qmidev,
                    void *data,
                    int status,
                    uint8_t cid)
{
  struct get_client_context *context = data;
  struct client *client = context->client;
  struct listen_criteria event_criteria;

  if (status) {
    g_slice_free(struct client, client);
    client = NULL;
    goto out;
  }

  client->client_id = cid;
  if (client->service->create_fn)
    client->service->create_fn(qmidev, client);

  event_criteria.flags = LISTEN_MATCH_QMI_FLAGS |
                         LISTEN_MATCH_MESSAGE;
  if (service_is_control(client->service))
    event_criteria.qmi_flags = QMI_FLAG_CTL_INDICATION;
  else
    event_criteria.qmi_flags = QMI_FLAG_SVC_INDICATION;
  event_criteria.message = client->service->event_msg_id;

  client->event_listener = qmidev_listen(qmidev, client,
                                         &event_criteria,
                                         &event_message_received,
                                         client);

out:
  context->callback(qmidev, context->context, status, client);
  g_slice_free(struct get_client_context, context);
}

int qmidev_get_client(struct qmidev *qmidev,
                      struct service *service,
                      qmidev_client_fn caller_callback,
                      void *caller_context)
{
  struct client *client;
  struct get_client_context *context;
  int result;

  client = g_slice_new(struct client);
  client->service = service;
  client->next_transaction_id = 0x0001;
  client->events = NULL;
  client->priv = NULL;

  context = g_slice_new(struct get_client_context);
  context->client = client;
  context->callback = caller_callback;
  context->context = caller_context;

  result = qmictl_get_cid(qmidev,
                          client->service->service_id,
                          &got_cid,
                          context);
  if (result) {
    g_slice_free(struct get_client_context, context);
    g_slice_free(struct client, client);
  }

  return result;
}

struct release_client_context {
  struct client *client;
  qmidev_response_fn callback;
  void *context;
};

static void released_cid(struct qmidev *qmidev,
                         void *data,
                         int status)
{
  struct release_client_context *context = data;
  struct client *client = context->client;

  if (status)
    goto out;

  qmidev_cancel_listen(qmidev, client->event_listener);

  /* TODO: Free things that reference client (transactions). */

  if (context->client->service->destroy_fn)
      context->client->service->destroy_fn(qmidev, context->client);
  g_slice_free(struct client, context->client);

out:
  context->callback(qmidev, context->context, status);
  g_slice_free(struct release_client_context, context);
}

int qmidev_release_client(struct qmidev *qmidev,
                          struct client *client,
                          qmidev_response_fn caller_callback,
                          void *caller_context)
{
  struct release_client_context *context;
  int result;

  context = g_slice_new(struct release_client_context);
  context->client = client;
  context->callback = caller_callback;
  context->context = caller_context;

  result = qmictl_release_cid(qmidev,
                              client->service->service_id,
                              client->client_id,
                              &released_cid,
                              context);
  if (result)
    g_slice_free(struct release_client_context, context);

  return result;
}

static gint client_compare_service(gconstpointer a, gconstpointer b)
{
  const struct client *client_a = a;
  const struct client *client_b = b;

  return client_a->service - client_b->service;
}

static struct client *find_client(struct qmidev *qmidev,
                                  struct service *service)
{
  struct client wanted;
  GList *found_list;

  wanted.service = service;
  found_list = g_list_find_custom(qmidev->clients,
                                  &wanted,
                                  &client_compare_service);

  if (found_list)
    return (struct client *) found_list->data;
  else
    return NULL;
}

struct client *qmidev_default_client(struct qmidev *qmidev,
                                     struct service *service)
{
  return find_client(qmidev, service);
}

static struct transaction *make_transaction(struct client *client,
                                            response_fn callback,
                                            void *context)
{
  struct transaction *transaction;

  transaction = g_slice_new(struct transaction);
  transaction->client = client;
  transaction->transaction_id = client->next_transaction_id;
  transaction->callback = callback;
  transaction->context = context;

  return transaction;
}

static void increment_transaction_id(struct client *client)
{
  static const uint16_t min_transaction_id = 0x0001;
  uint16_t max_transaction_id;

  if (client->service->service_id == QMI_SVC_CTL)
    max_transaction_id = 0x00FF;
  else
    max_transaction_id = 0xFFFF;

  if (client->next_transaction_id < max_transaction_id)
    client->next_transaction_id++;
  else
    client->next_transaction_id = min_transaction_id;
}

static int response_received(struct qmidev *qmidev,
                             void *data,
                             struct qmimsg *message)
{
  struct transaction *transaction = data;

  transaction->callback(qmidev,
                        transaction->context,
                        qmi_result(message),
                        message);
  g_slice_free(struct transaction, transaction);

  /* remove listener */
  return 1;
}

int qmidev_client_request(struct qmidev *qmidev,
                          struct client *client,
                          uint16_t msg_id,
                          request_fn make_request_fn,
                          void *request_context,
                          response_fn handle_response_fn,
                          void *response_context)
{
  struct transaction *transaction;
  struct qmimsg *request_message;
  struct listen_criteria response_criteria;
  int result;

  transaction = make_transaction(client, handle_response_fn, response_context);

  qmimsg_new(0, client->service->service_id, client->client_id,
             0, transaction->transaction_id, msg_id,
             &request_message);
  if (make_request_fn) {
    result = make_request_fn(request_context, request_message);
    if (result)
      goto fail;
  }

  response_criteria.flags = LISTEN_MATCH_TRANSACTION |
                            LISTEN_MATCH_QMI_FLAGS |
                            LISTEN_MATCH_MESSAGE;
  if (service_is_control(client->service))
    response_criteria.qmi_flags = QMI_FLAG_CTL_RESPONSE;
  else
    response_criteria.qmi_flags = QMI_FLAG_SVC_RESPONSE;
  response_criteria.transaction = client->next_transaction_id;
  response_criteria.message = msg_id;

  transaction->response_listener =
          qmidev_listen(qmidev, client,
                        &response_criteria,
                        &response_received,
                        transaction);

  /* TODO: Timeout. */

  result = qmidev_send(qmidev, request_message);

  increment_transaction_id(client);

  return 0;

fail:
  g_slice_free(struct transaction, transaction);
  qmimsg_free(request_message);
  return result;
}

int qmidev_request(struct qmidev *qmidev,
                   struct service *service,
                   uint16_t msg_id,
                   request_fn make_request_fn,
                   void *request_context,
                   response_fn handle_response_fn,
                   void *response_context)
{
  struct client *client;

  client = find_client(qmidev, service);
  if (!client)
    return QMI_ERR_SERVICE_UNAVAILABLE;

  return qmidev_client_request(qmidev, client, msg_id,
                               make_request_fn, request_context,
                               handle_response_fn, response_context);
}

struct void_context {
  qmidev_response_fn callback;
  void *context;
};

static void void_response_received(struct qmidev *qmidev,
                                   void *data,
                                   int result,
                                   struct qmimsg *message)
{
  struct void_context *context = data;

  /* nothing useful in message. */
  message = message;

  context->callback(qmidev, context->context, result);
  g_slice_free(struct void_context, context);
}

int qmidev_void_request(struct qmidev *qmidev,
                        struct service *service,
                        uint16_t msg_id,
                        request_fn make_request_fn,
                        void *request_context,
                        qmidev_response_fn handle_response_fn,
                        void *response_context)
{
  struct void_context *context;
  int result;

  context = g_slice_new(struct void_context);
  context->callback = handle_response_fn;
  context->context = response_context;

  result = qmidev_request(qmidev, service, msg_id,
                          make_request_fn, request_context,
                          &void_response_received, context);
  if (result)
    g_slice_free(struct void_context, context);
  return result;
}

void *client_priv(struct client *client)
{
  assert(client);
  return client->priv;
}

void client_set_priv(struct client *client, void *priv)
{
  assert(client);
  client->priv = priv;
}

void event_init(struct event *event, uint8_t type, event_fn callback)
{
  event->type = type;
  event->listener = NULL;
  event->callback = callback;
  event->user_callback = NULL;
  event->user_context = NULL;
}

struct set_callback_context {
  uint8_t event_type;
  uint8_t report_event;
  qmidev_response_fn callback;
  void *context;
};

static void trivial_set_callback_done(struct qmidev *qmidev,
                                      void *data)
{
  struct set_callback_context *context = data;
  context->callback(qmidev, context->context, 0);
  g_slice_free(struct set_callback_context, context);
}

int set_event_report_request(void *data,
                             struct qmimsg *message)
{
  struct set_callback_context *context = data;
  return qmimsg_tlv_add(message,
                        context->event_type,
                        sizeof(context->report_event),
                        &context->report_event);
}

void set_event_report_response(struct qmidev *qmidev,
                               void *data,
                               int result,
                               struct qmimsg *message)
{
  struct set_callback_context *context = data;

  /* nothing useful in message */
  message = message;

  context->callback(qmidev, context->context, result);
  g_slice_free(struct set_callback_context, context);
}

int qmidev_set_callback(struct qmidev *qmidev,
                        struct client *client,
                        struct event *event,
                        void *user_callback,
                        void *user_context,
                        qmidev_response_fn caller_callback,
                        void *caller_context)
{
  struct set_callback_context *context;
  int active_before, active_after;
  int result;

  active_before = !!event->user_callback;
  active_after = !!user_callback;

  context = g_slice_new(struct set_callback_context);
  context->event_type = event->type;
  context->report_event = active_after ? 0x01 : 0x00;
  context->callback = caller_callback;
  context->context = caller_context;

  event->user_callback = user_callback;
  event->user_context = user_context;

  if (active_before == active_after) {
    qmidev_call_later(qmidev, &trivial_set_callback_done, context);
    return 0;
  }

  if (active_after)
    client->events = g_list_prepend(client->events, event);
  else
    client->events = g_list_remove(client->events, event);

  result = qmidev_client_request(qmidev, client,
                                 client->service->event_msg_id,
                                 &set_event_report_request, context,
                                 &set_event_report_response, context);
  if (result)
    g_slice_free(struct set_callback_context, context);
  return result;
}


struct connect_context {
  GQueue *pending_services;

  qmidev_response_fn caller_callback;
  void *caller_context;
};

static void connect_next_service(struct qmidev *qmidev,
                                 struct connect_context *context);

static void got_client(struct qmidev *qmidev,
                       void *data,
                       int status,
                       struct client *client)
{
  struct connect_context *context = data;

  if (!status) {
    qmidev->clients = g_list_prepend(qmidev->clients, client);
  }

  connect_next_service(qmidev, context);
}

static void connect_next_service(struct qmidev *qmidev,
                                 struct connect_context *context)
{
  struct service *next_service;
  int result;

  assert(qmidev);
  assert(context);

  if (!g_queue_is_empty(context->pending_services)) {
    next_service = g_queue_pop_head(context->pending_services);
    result = qmidev_get_client(qmidev, next_service, &got_client, context);
    if (result)
      context->caller_callback(qmidev, context->caller_context, result);
  } else {
    context->caller_callback(qmidev, context->caller_context, 0);
    g_slice_free(struct connect_context, context);
  }
}

int qmidev_connect(struct qmidev *qmidev,
                   qmidev_response_fn caller_callback,
                   void *caller_context)
{
  struct connect_context *context;
  int result;

  assert(qmidev);
  assert(caller_callback);

  if (qmidev->connected)
    return QMI_ERR_ALREADY_CONNECTED;

  result = dev_open(qmidev->dev);
  /* TODO: Should this go through the callback instead? */
  if (result)
    return result;

  qmidev->connected = 1;

  qmidev->clients = g_list_prepend(qmidev->clients, make_ctl_client(qmidev));

  context = g_slice_new(struct connect_context);
  context->caller_callback = caller_callback;
  context->caller_context = caller_context;
  context->pending_services = g_queue_new();

  g_queue_push_tail(context->pending_services, &qmidms_service);
  g_queue_push_tail(context->pending_services, &qmiwds_service);

  connect_next_service(qmidev, context);

  return 0;
}

struct disconnect_context {
  qmidev_response_fn caller_callback;
  void *caller_context;
};

static void disconnected_services(struct qmidev *qmidev,
                                  struct disconnect_context *context)
{
  int result = dev_close(qmidev->dev);
  qmidev->connected = 0;
  context->caller_callback(qmidev, context->caller_context, result);
  g_slice_free(struct disconnect_context, context);
}

static void disconnect_next_service(struct qmidev *qmidev,
                                    struct disconnect_context *context);

static void released_client(struct qmidev *qmidev,
                            void *data,
                            int status)
{
  if (status) {
    /* TODO: What do we do if release fails? */
  }

  disconnect_next_service(qmidev, data);
}

static void disconnect_next_service(struct qmidev *qmidev,
                                    struct disconnect_context *context)
{
  struct client *next_client;
  int result;

  if (!qmidev->clients) {
    disconnected_services(qmidev, context);
    return;
  }

  next_client = qmidev->clients->data;
  qmidev->clients = g_list_remove(qmidev->clients, next_client);
  if (!next_client->client_id) {
    /* This is the CTL client.  Free it by hand and try the next one. */
    g_slice_free(struct client, next_client);
    disconnect_next_service(qmidev, context);
    return;
  }

  result = qmidev_release_client(qmidev, next_client,
                                 &released_client, context);
  if (result)
    context->caller_callback(qmidev, context->caller_context, result);
}

int qmidev_disconnect(struct qmidev *qmidev,
                      qmidev_response_fn caller_callback,
                      void *caller_context)
{
  struct disconnect_context *context;

  assert(qmidev);
  assert(caller_callback);

  if (!qmidev->connected)
    return QMI_ERR_NOT_CONNECTED;

  context = g_slice_new(struct disconnect_context);
  context->caller_callback = caller_callback;
  context->caller_context = caller_context;

  disconnect_next_service(qmidev, context);

  return 0;
}

static int dev_read_fn(void *ctx, void *buf, size_t len)
{
  struct qmidev *qmidev;
  assert(ctx);
  qmidev = ctx;
  assert(qmidev->dev);

  return dev_read(qmidev->dev, buf, len);
}

static void dev_read_cb(struct polled *polled)
{
  struct qmidev *qmidev;
  struct qmimsg *msg;
  int result;

  assert(polled);
  qmidev = polled_priv(polled);
  assert(qmidev);

  result = qmimsg_read(&dev_read_fn, qmidev, &msg);
  if (result)
    return;
  if (call_listeners(qmidev, msg) && qmidev_log_unhandled_messages) {
    fprintf(stderr, "Unhandled message received:\n");
    qmimsg_print(msg);
  }
  qmimsg_free(msg);
}

static void dev_close_cb(struct polled *polled)
{
  struct qmidev *qmidev;

  assert(polled);
  qmidev = polled_priv(polled);
  assert(qmidev);

  qmidev_clear_dev_fd(qmidev);
}

int qmidev_set_dev_fd(struct qmidev *qmidev, int fd)
{
  assert(qmidev);
  assert(fd >= 0);
  assert(!qmidev->dev_polled);

  qmidev->dev_polled = poller_add(qmidev->poller, fd);
  if (!qmidev->dev_polled)
    return QMI_ERR_INTERNAL;
  polled_set_priv(qmidev->dev_polled, qmidev);
  polled_set_read(qmidev->dev_polled, &dev_read_cb);
  polled_set_close(qmidev->dev_polled, &dev_close_cb);
  polled_update(qmidev->dev_polled);

  return 0;
}

int qmidev_clear_dev_fd(struct qmidev *qmidev)
{
  assert(qmidev);
  assert(qmidev->dev_polled);

  /* Make sure we don't get called again. */
  polled_set_close(qmidev->dev_polled, NULL);
  polled_update(qmidev->dev_polled);

  /* will be freed by the poller when the fd closes. */
  qmidev->dev_polled = NULL;

  return 0;
}

static int dev_write_fn(void *ctx, const void *buf, size_t len)
{
  struct qmidev *qmidev;

  assert(ctx);
  qmidev = ctx;
  assert(qmidev->dev);

  return dev_write(qmidev->dev, buf, len);
}

int qmidev_send(struct qmidev *qmidev, struct qmimsg *msg)
{
  assert(qmidev);
  assert(qmidev->connected);

  return qmimsg_write(msg, &dev_write_fn, qmidev);
}

struct polled *qmidev_poll(struct qmidev *qmidev, int fd)
{
  assert(qmidev);

  return poller_add(qmidev->poller, fd);
}



void qmidev_call_later(struct qmidev *qmidev, callback_fn func, void *context)
{
  struct callback *callback;

  assert(qmidev);
  assert(func);

  callback = g_slice_new(struct callback);
  callback->func = func;
  callback->context = context;
  qmidev->callbacks = g_list_append(qmidev->callbacks, callback);
}

static void call_callback(void *data, void *user_data)
{
  struct qmidev *qmidev = user_data;
  struct callback *callback = data;

  callback->func(qmidev, callback->context);
  g_slice_free(struct callback, callback);
}

static void call_callbacks(struct qmidev *qmidev)
{
  g_list_foreach(qmidev->callbacks, call_callback, qmidev);
  g_list_free(qmidev->callbacks);
  qmidev->callbacks = NULL;
}
